goog.provide('dk.BookEditor');
goog.provide('dk.BookEditor.ViewType');

goog.require('gdata.docs.ui');
goog.require('dk.bookeditor.templates');
goog.require('dk.cursorManager');
goog.require('dk.html');
goog.require('dk.xml');

goog.require('goog.Disposable');
goog.require('goog.dispose');
goog.require('goog.dom');
goog.require('goog.dom.DomHelper');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TextRange');
goog.require('goog.dom.classes');
goog.require('goog.dom.selection');
goog.require('goog.events.EventHandler');
goog.require('goog.events.KeyCodes');
goog.require('goog.events.KeyHandler');
goog.require('goog.events.KeyHandler.EventType');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.Component.EventType');
goog.require('goog.ui.decorate');
goog.require('goog.userAgent');
goog.require('soy');


/**
 * @param {!gdata.auth.User} user
 * @param {string} logoutUrl
 * @param {?Object} documentIds
 * @param {!goog.dom.DomHelper} dom
 * @constructor
 * @extends {goog.ui.Component}
 */
dk.BookEditor = function(user, logoutUrl, documentIds, dom) {
  goog.base(this, dom);

  /**
   * @type {!gdata.auth.User} user
   * @private
   */
  this.user_ = user;

  /**
   * @type {string}
   * @private
   */
  this.logoutUrl_ = logoutUrl;
  
  /**
   * @type {Object}
   * @private
   */
  this.documentIds_ = documentIds;
};
goog.inherits(dk.BookEditor, goog.ui.Component);

/** @enum {string} */
dk.BookEditor.ViewType = {
  EDITOR: 'e',
  XML: 'x',
  HTML: 'h'
};

/**
 * @type {!dk.BookEditor.ViewType}
 * @private
 */
dk.BookEditor.prototype.currentView_ = dk.BookEditor.ViewType.EDITOR;

/**
 * The DocBook XML representation of the last saved version of the current
 * document. 
 * @type {?string}
 * @private
 */
dk.BookEditor.prototype.xml_ = null;

/**
 * The HTML representation of the last saved version of the current document. 
 * @type {?string}
 * @private
 */
dk.BookEditor.prototype.html_ = null;

/**
 * @type {gdata.docs.ui.DocViewer}
 * @private
 */
dk.BookEditor.prototype.viewer_;

/**
 * @type {?gdata.docs.ui.DocPicker}
 * @private
 */
dk.BookEditor.prototype.picker_;

/**
 * @type {goog.ui.Control}
 * @private
 */
dk.BookEditor.prototype.newDocumentButton_;

/** @inheritDoc */
dk.BookEditor.prototype.decorateInternal = function(element) {
  var soyData = {
    email: this.user_.getEmail(),
    logoutUrl: this.logoutUrl_
  };
  soy.renderElement(element, dk.bookeditor.templates.base, soyData);

  var dom = this.dom_;

  var viewerEl = dom.getElement('viewer');
  this.viewer_ = new gdata.docs.ui.DocViewer(viewerEl, {stripHeadHtml: true});

  var pickerEl = dom.getElement('picker');
  this.picker_ = new gdata.docs.ui.DocPicker(
      this.user_,
      pickerEl,
      this.viewer_,
      {documentIdFilter: this.documentIds_});
  // Fix the size of the picker iframe so that long titles wrap within it.
  pickerEl.firstChild.style.width = '200px';

  this.newDocumentButton_ = goog.ui.decorate(dom.getElement('new-document'));
  this.saveButton_ = goog.ui.decorate(dom.getElement('save'));
  this.editButton_ = goog.ui.decorate(dom.getElement('edit'));
  this.showHtmlButton_ = goog.ui.decorate(dom.getElement('show-html'));
  this.showXmlButton_ = goog.ui.decorate(dom.getElement('show-xml'));
};

/** @inheritDoc */
dk.BookEditor.prototype.enterDocument = function() {
  goog.base(this, 'enterDocument');

  var viewer = this.viewer_;
  var postLoadProcessor = goog.bind(this.makeDivsContentEditable_, this);
  viewer.setPostLoadProcessor(postLoadProcessor);
  viewer.setPreSaveProcessor(function(content, doc) {
    var domHelper = new goog.dom.DomHelper(doc);
    var brs = domHelper.getElementsByTagNameAndClass('br');
    for (var i = 0; i < brs.length; ++i) {
      var br = brs[i];
      if (!br.nextSibling) {
        goog.dom.removeNode(br);
      } else {
        var textNode = doc.createTextNode('\n');
        goog.dom.replaceNode(textNode, br);
      }
    }
    return doc.body.innerHTML;
  });

  var dom = this.dom_;
  var handler = this.getHandler();

  handler.listen(
      this.newDocumentButton_,
      goog.ui.Component.EventType.ACTION,
      this.onNewDocument_);

  handler.listen(
      this.saveButton_,
      goog.ui.Component.EventType.ACTION,
      this.save_);

  handler.listen(
      this.editButton_,
      goog.ui.Component.EventType.ACTION,
      goog.partial(this.showView_, dk.BookEditor.ViewType.EDITOR));

  handler.listen(
      this.showHtmlButton_,
      goog.ui.Component.EventType.ACTION,
      goog.partial(this.showView_, dk.BookEditor.ViewType.HTML));

  handler.listen(
      this.showXmlButton_,
      goog.ui.Component.EventType.ACTION,
      goog.partial(this.showView_, dk.BookEditor.ViewType.XML));

  handler.listen(
      viewer,
      gdata.docs.ui.DocViewer.EventType.DOCUMENT_LOADED,
      function(e) {
          var title = e.target.getEntry().getTitle();
          dom.getDocument().title = title;
          dom.getElement('title').innerHTML = goog.string.htmlEscape(title);
          this.onResize_();
          this.tryUpdateXml_();
      });

  handler.listen(
      dom.getWindow(),
      goog.events.EventType.RESIZE,
      this.onResize_);
  this.onResize_();
};

/**
 * Handles the "new document" action.
 * @param {goog.events.Event} e
 * @private
 */
dk.BookEditor.prototype.onNewDocument_ = function(e) {
  var title = prompt('Enter a name for the new document:');
  if (title == null || !(title = goog.string.trim(title))) return;
  var deferred = this.picker_.getDocumentListFeed().createDocument(title);
  deferred.addCallback(this.onNewDocumentSuccess_, this);
};

/**
 * @param newDocId
 * @private
 */
dk.BookEditor.prototype.onNewDocumentSuccess_ = function(newDocId) {
  this.picker_.refresh(newDocId);
};

/**
 * Shows the specified view by hiding/showing the appropriate HTML elements.
 * @param {!dk.BookEditor.ViewType} viewType
 * @param {!goog.events.Event} e ACTION event that triggered the view change
 * @private
 */
dk.BookEditor.prototype.showView_ = function(viewType, e) {
  var clickedButton = /** @type {goog.ui.ToggleButton} */ (e.target);
  if (viewType == this.currentView_) {
    // Do not allow user to uncheck a checked button by clicking on it
    // (as radio buttons do).
    clickedButton.setChecked(true);
    return;
  }
  
  var buttons = [
    this.editButton_,
    this.showHtmlButton_,
    this.showXmlButton_
  ];
  goog.array.forEach(buttons, function(b) {
    b.setChecked(clickedButton === b);
  });

  this.currentView_ = viewType;

  // TODO(bolinfest): This logic seems a bit sloppy, particularly with respect
  // to showing/hiding the #viewer and its corresponding iframe. Find a way to
  // tighten it up.

  var dom = this.dom_;
  // Firefox has a bug where if an iframe is display:none and then re-displayed,
  // it will no longer show the cursor when it has focus.
  var viewerEl = dom.getElement('viewer');
  if (viewType != dk.BookEditor.ViewType.EDITOR) {
    viewerEl.style.height = 0;
    var viewerIframe = viewerEl.firstChild;
    if (viewerIframe) viewerIframe.style.height = '0';
  } else {
    viewerEl.style.height = '';
    this.onResize_();
  }
  goog.style.showElement(
      dom.getElement('xml-viewer'), viewType == dk.BookEditor.ViewType.XML);
  goog.style.showElement(
      dom.getElement('html-viewer'), viewType == dk.BookEditor.ViewType.HTML);

  // If switching to the XML view, highlight all of the XML so the user can
  // easily copy it to the clipboard and paste it into another document.
  if (viewType == dk.BookEditor.ViewType.XML && this.xml_) {
    var pre = dom.getElement('xml-viewer').firstChild;
    var range = goog.dom.TextRange.createFromNodeContents(pre);
    setTimeout(function() { range.select(); }, 0);
  }
};

/**
 * @param {goog.events.BrowserEvent=} e
 * @private
 */
dk.BookEditor.prototype.onResize_ = function(e) {
  var dom = this.dom_;
  var headerHeight = goog.style.getSize(dom.getElement('header')).height;
  var viewportSize = dom.getViewportSize();
  var viewportHeight = viewportSize.height;
  var height = viewportHeight - headerHeight;
  var viewerEl = this.dom_.getElement('content-viewer');
  viewerEl.style.height = height + 'px';

  // For some reason, the iframe has to be shorter than viewer element to avoid
  // triggering a vertical scrollbar.
  // TODO(bolinfest): Figure out what is causing this.
  var offset = goog.userAgent.GECKO ? 4 : (goog.userAgent.WEBKIT ? 3 : 0); 
  var iframeHeight = (height - offset) + 'px';

  // Adjust the height of the docpicker iframe.
  dom.getElement('picker').firstChild.style.height = iframeHeight + 'px';
  
  // Adjust the height of the editor iframe.
  var viewerIframe = dom.getElement('viewer').firstChild;  
  if (viewerIframe && this.currentView_ == dk.BookEditor.ViewType.EDITOR) {
    viewerIframe.style.height = iframeHeight;
  }

  // Adjust the height of the HTML viewer.
  dom.getElement('html-viewer').style.height = height + 'px';

  // Adjust the height of the XML viewer.
  dom.getElement('xml-viewer').style.height = height + 'px';

  // Adjust the width of the right column.
  // Add up the widths of the left and center columns (which are fixed width)
  // and give the right column the remaining space (but give it a minimum width).
  var viewportWidth = viewportSize.width;
  var leftcolWidth = goog.style.getSize(dom.getElement('leftcol')).width;
  var viewerWidth = goog.style.getSize(dom.getElement('content-viewer')).width;
  var rightColPaddingLeft = 5;
  var minRightColWidth = 284;
  var rightColWidth = Math.max(minRightColWidth,
      (viewportWidth - viewerWidth - leftcolWidth - rightColPaddingLeft));
  dom.getElement('rightcol').style.width = rightColWidth + 'px';
};

/**
 * @private
 */
dk.BookEditor.prototype.save_ = function() {
  var dom = this.dom_;
  var statusEl = dom.getElement('status');
  statusEl.innerHTML = 'Saving...';
  try {
    this.tryUpdateXml_();

    var onSave = function(serverResponse) {
      // TODO: may want to print out serverResponse for debugging help
      statusEl.innerHTML = 'Saved at: ' + (new Date());
    };

    var onFailedSave = function(errorMessage) {
      var message = '<span style="color: red; font-weight: bold;">' +
          'Error while saving to Google Docs: ' + errorMessage + '</span>';
      alert(message);
      statusEl.innerHTML = message;
    };

    this.viewer_.save(onSave, onFailedSave);
  } catch (e) {
    alert('Failed to save to local file: ' + e);
    statusEl.innerHTML =
        '<span style="color: red; font-weight: bold;">Failed to save!</span>';
  }
};

/**
 * Tries to generate DocBook XML from the current document.
 * @private
 */
dk.BookEditor.prototype.tryUpdateXml_ = function() {
  var dom = this.dom_;
  var xml = null;
  var html = null;
  try {
    var title = dom.getDocument().title;
    var iframe = dom.getElement('viewer').firstChild;
    var chapterDoc = goog.dom.getFrameContentDocument(iframe);
    var domHelper = new goog.dom.DomHelper(chapterDoc);

    // TODO: Let user specify id, byline, isAppendix
    var options = {
      title: title,
      domHelper: domHelper,
      id: title,
      byline: null,
      isAppendix: false
    };
    xml = dk.xml.generateDocBookXml(options);
    html = dk.html.generateHtml(options);
  } catch (e) {
    alert(e);
  }
  this.xml_ = xml;
  this.html_ = html;

  var xmlViewerEl = dom.getElement('xml-viewer');
  if (xml != null) {
    xmlViewerEl.innerHTML = '<pre class="xml">' + goog.string.htmlEscape(xml) + '</pre>';
  } else {
    xmlViewerEl.innerHTML = '<b>There was an error generating the XML</b>';
  }

  var htmlViewerEl = dom.getElement('html-viewer');
  if (html != null) {
    htmlViewerEl.innerHTML = html;
  } else {
    htmlViewerEl.innerHTML = '<b>There was an error generating the HTML</b>';
  }
};

/**
 * Finds all elements with the "editable" class and sets content-editable to
 * true. Adds a key handler to these editable elements and inserts CSS to
 * style the document appropriately.
 * @param {Document} doc This is the document of the iframe, which is different
 *     than the document that this editor belongs to.
 * @private
 */
dk.BookEditor.prototype.makeDivsContentEditable_ = function(doc) {
  var dom = new goog.dom.DomHelper(doc);

  // Install editorframe.css stylesheet.

  var divs = dom.getElementsByTagNameAndClass(undefined, 'editable');
  for (var i = 0; i < divs.length; ++i) {
    var div = divs[i];
    div.contentEditable = true;
    var html = div.innerHTML;
    html = goog.string.trim(html);
    div.innerHTML = html;
  }
  var body = dom.getDocument().body;
  var keyHandlerEl = body;
  var keyHandler = new goog.events.KeyHandler(keyHandlerEl);
  var handler = this.getHandler();
  handler.listen(keyHandler,
                 goog.events.KeyHandler.EventType.KEY,
                 this.onKeyEvent_);
  
  handler.listen(body, goog.events.EventType.CLICK, this.onClick_);
};

/**
 * @param {goog.events.BrowserEvent} e
 * @private
 */
dk.BookEditor.prototype.onClick_ = function(e) {
  var el = e.target;
  if (goog.dom.classes.has(el, 'include')) {
    var text = goog.dom.getRawTextContent(el);
    var rect = goog.style.getBounds(el);
    var doc = goog.dom.getDomHelper(el).getDocument();
    // TODO(bolinfest): Remove Mozilla-specific dependencies.
    var options = {
      width: 630,
      height: 500,
      left:  window.mozInnerScreenX,
      top:  window.mozInnerScreenY + rect.top + rect.height - doc.body.scrollTop,
      target: goog.window.DEFAULT_POPUP_TARGET + goog.string.getRandomString()
    };
    goog.window.open('file://' + DIR + text, options);
  }
}

/**
 * @param {!Element} editableEl
 * @param {boolean} previous
 * @return {Element?}
 */
function findEditableEl(editableEl, previous) {
  // Look 3 nodes in the specified direction before giving up.
  var i = 3;
  do {
    editableEl = editableEl[previous ? 'previousSibling' : 'nextSibling'];
    if (!editableEl) {
      return null;
    }
    if (editableEl.nodeType == goog.dom.NodeType.ELEMENT &&
        goog.dom.classes.has(editableEl, 'editable')) {
      return editableEl;
    }
    --i;
  } while (i >= 0);
  return null;
}


var switchMode = function(contentEditableEl, text, isCycleForward) {
  var domHelper = goog.dom.getDomHelper(contentEditableEl);
  // CSS styles for these classes are defined in editorframe.css.
  var classes = ['paragraph',
                 'source-code',
                 'include',
                 'big-heading',
                 'heading',
                 'small-heading',
                 'list-item'];
  var i;
  for (i = 0; i < classes.length; ++i) {
    if (goog.dom.classes.has(contentEditableEl, classes[i])) {
      break;
    }
  }
  if (i >= classes.length) {
    throw new Error('contentEditableEl does not contain expected class');
  }
  var oldClass = classes[i];
  var deltaIndex = isCycleForward ? 1 : -1;
  var numClasses = classes.length;
  var newClass = classes[(i + deltaIndex + numClasses) % numClasses];
  
  var el = domHelper.createElement(
      (newClass == 'source-code' || newClass == 'include') ? 'pre' : 'div');
  el.className = contentEditableEl.className;
  goog.dom.classes.swap(el, oldClass, newClass);
  goog.dom.setTextContent(el, text);
  el.contentEditable = true;
  goog.dom.replaceNode(el, contentEditableEl);
  el.focus();
};

/**
 * Handles a key event in the editor.
 * @param {goog.events.KeyEvent} e
 * @private
 */
dk.BookEditor.prototype.onKeyEvent_ = function(e) {
  var keyCode = e.keyCode;
  var contentEditableEl = e.target;
  var cursorPosition = dk.cursorManager.getCursorPosition(contentEditableEl);
  var domHelper = goog.dom.getDomHelper(contentEditableEl);
  var text = dk.cursorManager.nodeToText(contentEditableEl);
  //  output('keyCode: ' + e.keyCode + '; cursorPos: ' + cursorPosition +
  //      '; text.length: ' + text.length);

  if (keyCode == goog.events.KeyCodes.ENTER) {
    if (e.ctrlKey && !e.shiftKey) {
      e.preventDefault();
      switchMode(contentEditableEl, text, true /* cycleForward */);
    } else if (e.ctrlKey && e.shiftKey) {
      e.preventDefault();
      switchMode(contentEditableEl, text, false /* cycleForward */);
    } else if (e.shiftKey) {
      // shift-enter creates a new paragraph.
      e.preventDefault();
      var newDiv = domHelper.createElement('div');
      newDiv.contentEditable = true;
      newDiv.className = 'editable paragraph';
      newDiv.innerHTML = ' ';
      goog.dom.insertSiblingAfter(newDiv, contentEditableEl);
      newDiv.focus();
    }
  } else if (keyCode == goog.events.KeyCodes.UP) {
    if (dk.cursorManager.isCursorInRow(contentEditableEl, true /*isTop*/)) {
      var previousEl = findEditableEl(contentEditableEl, true /*previous*/);
      if (previousEl) {
        e.preventDefault();
        previousEl.focus();
      }
    }
  } else if (keyCode == goog.events.KeyCodes.DOWN) {
    if (dk.cursorManager.isCursorInRow(contentEditableEl, false /*isTop*/)) {
      var nextEl = findEditableEl(contentEditableEl, false /*previous*/);
      if (nextEl) {
        e.preventDefault();
        nextEl.focus();
      }
    }
  } else if (keyCode == goog.events.KeyCodes.RIGHT) {
    // Hitting right at the end of the line should bring the cursor to the
    // beginning of the next editable element.
    if (text.length == cursorPosition) {
      var elToFocus = findEditableEl(contentEditableEl, false /*previous*/);
      if (elToFocus) {
        e.preventDefault();
        elToFocus.focus();
      }
    }
  } else if (keyCode == goog.events.KeyCodes.LEFT) {
    // Hitting left at the beginning of the line should bring the cursor to the
    // end of the previous editable element.
    if (cursorPosition == 0) {
      var elToFocus = findEditableEl(contentEditableEl, true /*previous*/);
      if (elToFocus) {
        e.preventDefault();
        dk.cursorManager.placeCursorAtEnd(elToFocus);
      }
    }
  } else if (keyCode == goog.events.KeyCodes.S && e.ctrlKey) {
    e.preventDefault();
    // ctrl-S saves the file.
    this.save_();
  } else if (keyCode == goog.events.KeyCodes.BACKSPACE) {
    // Get the HTML of the element, strip all <br> elements, and delete this
    // element if there is no other content.
    var html = contentEditableEl.innerHTML;
    html = html.replace(/<br[^>]*>/g, '');
    if (goog.string.isEmpty(html)) {
      e.preventDefault();
      var elToFocus = findEditableEl(contentEditableEl, true /*previous*/);
      if (!elToFocus) {
        elToFocus = findEditableEl(contentEditableEl, false /*previous*/);
      }
      contentEditableEl.blur();
      goog.dom.removeNode(contentEditableEl);
      if (elToFocus) {
        elToFocus.focus();
      }
    }
  }

  // TODO(bolinfest): Use apple up/down to swap paragraphs like in Tasks.
};

/** @inheritDoc */
dk.BookEditor.prototype.disposeInternal = function() {
  goog.base(this, 'disposeInternal');
  goog.dispose(this.viewer_);
  goog.dispose(this.picker_);
  goog.dispose(this.newDocumentButton_);
  goog.dispose(this.saveButton_);
  goog.dispose(this.editButton_);
  goog.dispose(this.showHtmlButton_);
  goog.dispose(this.showXmlButton_);
};
